using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;

namespace Comms {
    public class LimiterTransmitter : IWrapperTransmitter {
        public Alarm Alarm;

        public volatile bool IsDisposed;

        public int BytesPerSecondLimit;

        public float Period;

        public double BytesRemaining;

        public double BytesRemainingTime;

        public Queue<Packet> QueuedPackets = new();

        public long QueuedBytes;

        public object Lock = new();

        public ITransmitter BaseTransmitter { get; }

        public int MaxPacketSize => BaseTransmitter.MaxPacketSize;

        public IPEndPoint Address => BaseTransmitter.Address;

        public event Action<Exception> Error;

        public event Action<string> Debug;

        public event Action<Packet> PacketReceived;

        public LimiterTransmitter(ITransmitter baseTransmitter, int bytesPerSecondLimit, float period = 0.01f) {
            BaseTransmitter = baseTransmitter ?? throw new ArgumentNullException("baseTransmitter");
            if (period < 0.005f
                || period > 0.1f) {
                throw new ArgumentException("period too small or too large.");
            }
            BytesPerSecondLimit = bytesPerSecondLimit;
            Period = period;
            BaseTransmitter.Error += InvokeError;
            BaseTransmitter.Debug += delegate(string s) { Debug?.Invoke(s); };
            BaseTransmitter.PacketReceived += delegate(Packet packet) { PacketReceived?.Invoke(packet); };
            Alarm = new Alarm(AlarmFunction);
            Alarm.Error += InvokeError;
            BytesRemaining = bytesPerSecondLimit * Period;
            BytesRemainingTime = Comm.GetTime();
        }

        public void Dispose() {
            if (!IsDisposed) {
                IsDisposed = true;
                BaseTransmitter.Dispose();
                Alarm.Dispose();
            }
        }

        public void SendPacket(Packet packet) {
            CheckNotDisposed();
            lock (Lock) {
                if (QueuedPackets.Count > 0) {
                    QueuedPackets.Enqueue(packet);
                }
                else if (!TrySend(packet)) {
                    QueuedPackets.Enqueue(packet);
                    Alarm.Set(Period);
                }
            }
        }

        public void AlarmFunction() {
            lock (Lock) {
                if (IsDisposed) {
                    return;
                }
                while (QueuedPackets.Count > 0) {
                    Packet packet = QueuedPackets.Peek();
                    if (TrySend(packet)) {
                        QueuedPackets.Dequeue();
                        QueuedBytes -= packet.Bytes.Length;
                        continue;
                    }
                    Alarm.Set(Period);
                    break;
                }
            }
        }

        public void Enqueue(Packet packet) {
            QueuedPackets.Enqueue(packet);
            QueuedBytes += packet.Bytes.Length;
            _ = QueuedBytes;
            _ = BytesPerSecondLimit * 10;
        }

        public bool TrySend(Packet packet) {
            double time = Comm.GetTime();
            double val = Math.Max(2 * BytesPerSecondLimit * Period, 2 * MaxPacketSize);
            BytesRemaining = Math.Min(BytesRemaining + (time - BytesRemainingTime) * BytesPerSecondLimit, val);
            BytesRemainingTime = time;
            if (packet.Bytes.Length <= BytesRemaining) {
                BytesRemaining -= packet.Bytes.Length;
                BaseTransmitter.SendPacket(packet);
                return true;
            }
            return false;
        }

        public void CheckNotDisposed() {
            if (IsDisposed) {
                throw new ObjectDisposedException("LimiterTransmitter");
            }
        }

        public void InvokeError(Exception error) {
            Error?.Invoke(error);
        }

        [Conditional("DEBUG")]
        public void InvokeDebug(string message) {
            Debug?.Invoke(message);
        }

        [Conditional("DEBUG")]
        public void InvokeDebug(string format, params object[] args) {
            if (Debug != null) {
                Debug?.Invoke(string.Format(format, args));
            }
        }
    }
}